/**
 * Filter
 * Censors *** words.
 *
 * Cassie Chin, Stephen Smetana, Logan Widick
 * CSCE 3530 Program 2
 */

#define _GNU_SOURCE // get rid of the compile warning when using strcasestr
#include <string.h>
#include <stdlib.h> 
#include <stdbool.h>
#include <ctype.h>
#include "filter.h"

// the # of chars to check to see if the file identifies a text file
#define NUM_CHARS_TO_CHECK 100
#define FILTER_BUFFER_SIZE 1024

/**
 * \struct Word
 * A Word is an item in a list of words that will be filtered.
 */
typedef struct Word {
	/** the source word */
	char *uncensored;
	struct Word *next;
} Word;

/**
 * \struct Filter
 * A Filter contains a list of words to be filtered.
 */
typedef struct Filter {
	Word *head;
	int size;
} Filter;

/**
 * Constructor for a new word 
 * 
 * @param uncensored The uncensored word.
 */
Word *new_Word(char *uncensored) {
	if (!uncensored)
		return NULL;

	Word *new = (Word *) calloc(1, sizeof(Word));
	new->uncensored = strdup(uncensored);

	return new;
}

/**
 * Inserts a word into the filter
 * 
 * @param filter The filter to insert into
 * @param uncensored The word to insert into the filter.
 */
void Filter_insert(Filter *filter, char *uncensored) {
	Word *new = new_Word(uncensored);

	// Insert the head into the filter
	if (!filter->head) {
		filter->head = new;
		filter->size++;
	}
	// Search through the filter and insert it alphabetically
	else {
		Word *ptr;
		for (ptr = filter->head; ptr; ptr = ptr->next) {
			if (ptr->next == NULL) {
				ptr->next = new;
				filter->size++;
				return;
			} else if (strcasecmp(uncensored, ptr->next->uncensored) < 0) {
				new->next = ptr->next;
				ptr->next = new;
				filter->size++;
				break;
			}
		}
	}
}

/**
 * Constructor for the filter.
 *
 * @param fp The file containing the words to put into the filter.
 */
Filter *new_Filter(FILE *fp) {
	Filter *filter = (Filter *) calloc(1, sizeof(Filter));

	if (!filter)
		return NULL;
	if (!fp)
		return NULL;

	char buffer[FILTER_BUFFER_SIZE + 2];
	int i = 0;
	while (fgets(buffer, FILTER_BUFFER_SIZE, fp)) {
		//remove trailing newline
		i = strlen(buffer) - 1;
		while (buffer[i] == '\r' || buffer[i] == '\n') {
			buffer[i] = '\0';
			i--;
		}

		//add to filter.
		Filter_insert(filter, buffer);
	}

	return filter;
}

/**
 * Filter destructor.
 * @param filter The filter to delete.
 */
void delete_Filter(Filter *filter) {
	if (!filter)
		return;
	Word *ptr = NULL;
	Word *ptr_next = NULL;

	ptr = filter->head;
	while (ptr) {
		ptr_next = ptr->next;
		free(ptr->uncensored);
		free(ptr);
		ptr = ptr_next;
	}
	free(filter);
}

/**
 * Determines whether a file is a text file. If everything in the file is a character or a new line then this is a file.
 * 
 * @param filter The filter to use
 * @param theFile Check to see if this is a text file or not.
 */
bool Filter_isTextFile(Filter *filter, FILE *theFile) {
	if (!filter || !theFile)
		return false;
	rewind(theFile);

	int i = 0;
	int theChar = 0;
	for (i = 0; i < NUM_CHARS_TO_CHECK; i++) {
		theChar = fgetc(theFile);
		//we reach eof: can vouch that the file is text.
		if (theChar == EOF)
			return true;
		// new line is not a character yet it is a valid symbol so continue
		else if (theChar == '\n' || theChar == '\r')
			continue;
		//we reach a non-printable; exit.
		else if (!isprint(theChar))
			return false;
	}
	return true;
}

/**
 * Filters a buffer by replacing all the bad words with stars.
 *
 * @param filter The filter to use.
 * @param rawText The buffer to filter.
 */
void Filter_filterLine(Filter *filter, char *rawText) {
	if (!filter || !rawText)
		return;

	char *match = NULL; // The bad word that was found in the buffer
	char *uncensored = NULL; // The word in the filter to find
	int uncensored_len = 0;
	Word *curr_word = NULL;
	char *currStartOfString = rawText; // A pointer that moves down the buffer
	bool beginOk = false; // this is true if the character before the match is a space or punctuation
	bool endOk = false; // this is true if the character after the match is a space or punctuation

	// go through the words
	for (curr_word = filter->head; curr_word != NULL;
			curr_word = curr_word->next) {
		uncensored = curr_word->uncensored;
		if (!uncensored)
			continue;

		currStartOfString = rawText; // reset the pointer to the start of the rawText
		uncensored_len = strlen(uncensored);

		//go through the line to replace all instances.
		while (1) {
			match = strcasestr(currStartOfString, uncensored);
			if (match == NULL)
				break;

			// The match is the first word in the string so you don't need to check for index -1
			if (match == currStartOfString) {
				beginOk = true;
			}
			// If the index before the match is a space or is punctuation, then it is ok
			else if (match > currStartOfString && match[-1] != '/'
					&& (isspace(match[-1]) || ispunct(match[-1]))) {
				beginOk = true;
			}

			// If the index after the match is a space or is punctuation, then it is ok
			if (isspace(match[uncensored_len]) || match[uncensored_len] == '\0'
					|| ispunct(match[uncensored_len])) {
				endOk = true;
			}
			//but if the index after the match is a /, then it is baaad.
			if (match[uncensored_len] == '/')
				endOk = false;

			// Only filter if the word is valid (aka, it's not part of another word like... class, cassie, or widick
			if (beginOk && endOk) {
				int i;
				for (i = 0; i < uncensored_len; i++) {
					match[i] = '*'; // replace all characters with a star
				}
			}

			beginOk = false; // reset boolean values for next strcasestr check
			endOk = false;
			currStartOfString = match + strlen(uncensored); // move the pointer that strstr will look through
		}
	}
}

/**
 * Replaces all bad words in the inFile and saves the results in outFile.
 * 
 * @param filter The filter to use
 * @param inFile The file to read
 * @param outFile The filtered version of the input file.
 */
void Filter_filterFile(Filter *filter, FILE* inFile, FILE* outFile) {
	if (!filter || !inFile || !outFile)
		return;
	rewind(inFile);
	rewind(outFile);

	if (!Filter_isTextFile(filter, inFile)) {
		char ch;
		// Copy infile into outfile
		while ((ch = fgetc(inFile)) != EOF)
			fputc(ch, outFile);
		return;
	}

	rewind(inFile);
	rewind(outFile);

	char linebuffer[FILTER_BUFFER_SIZE + 2];
	// Read in one line from the buffer
	while (fgets(linebuffer, FILTER_BUFFER_SIZE, inFile)) {
		// line buffer may contain bad words
		Filter_filterLine(filter, linebuffer);
		// line buffer should have stars over bad words
		fputs(linebuffer, outFile); // write the output to the outFile
	}
}
